#! /usr/bin/env python

# Copyright (c) 2010 Chris LeBaron
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from termcolor import colored, cprint
import random

LATS = range(32)
LONGS = range(30)

#=============================================================================
def adjacent(tile):
    lat, long = tile
    N = lat-1 if lat>0 else LATS[-1]
    S = lat+1 if lat<LATS[-1] else 0
    E = long+1 if long<LONGS[-1] else 0
    W = long-1 if long>0 else LONGS[-1]
    return (N,long), (S,long), (lat,W), (lat,E)

#=============================================================================
def king_move(tile):
    lat, long = tile
    N = lat-1 if lat>0 else LATS[-1]
    S = lat+1 if lat<LATS[-1] else 0
    E = long+1 if long<LONGS[-1] else 0
    W = long-1 if long>0 else LONGS[-1]
    return (N,long), (S,long), (lat,W), (lat,E), (N,W), (N,E), (S,W), (S,E)

#=============================================================================
def two_away(tile):
    lat, long = tile
    return king_move(tile)+((lat,long-2),(lat,long+2),(lat-2,long),(lat+2,long))


#=============================================================================
class Config:
    def __init__(self):
        self.plates = 150
        self.exclude = [(lat,long) for lat in 2,LATS[-3] for long in LONGS]
        self.len_continental = 300
        self.biome_size = 0.5 #1.0 is random, 0.0 is large biomes
        self.rivers = 50
        self.biome_dist = 'ggggggppppffff---++'
        self.biome_var = {
            '-':
                '''aaat ooot aat at at 
                   at at at t t
                   td td d d d
                   d d d d d
                   d d d d d'''.split(),
            '*':
                '''s s s s s
                   s s s s s
                   s s s s sj
                   sj sj sj sj sj
                   j j j j j'''.split(),
            '+':
                'mhh'}
    def earthlike(self):
        self.exclude += [(x,30) for x in LATS[3:-3]]
        return self


#=============================================================================
class World:
    def __init__(self, config=Config().earthlike()):
        self.config = config
        self.grid = [[-1] * len(LONGS) for lat in LATS]

        plates = self.config.plates
        self.plate = [[] for p in xrange(plates)]
        self.continental = []
        self.biomes = []
        self.river = []
        self.map = []
        
    #=========================================================================
    def lay_plates(self):
        borders = {}
        def place(plate, tile):
            lat, long = tile
            self.grid[lat][long] = plate
            self.plate[plate].append(tile)
            try: del borders[tile]
            except KeyError: pass
            cands = [(clat,clong) for clat, clong in adjacent(tile)
                     if self.grid[clat][clong] == -1]
            for t in cands:
                borders[t] = borders.setdefault(t, []) + [plate]

        coords = [(lat,long) for lat in LATS for long in LONGS]
        for plate, tile in enumerate(random.sample(coords, self.config.plates)):
            place(plate, tile)
        while borders:
            coord, cs = random.choice(borders.items())
            c = random.choice(cs)
            place(c, coord)

    #=========================================================================
    def continental_plates(self):
        x = [(lat,long) for lat in LATS[:2]+LATS[-2:] for long in LONGS]
        x += [(lat,long) for long in LONGS[:2]+LONGS[-2:] for lat in LATS[3:-3]]
        x += self.config.exclude
        x_off = set()
        for lat, long in x:
            x_off.add(self.grid[lat][long])

        land = 0
        goal = self.config.len_continental
        for p in set(range(len(self.plate))) - x_off:
            if abs(land+len(self.plate[p]) - goal) > abs(land - goal):
                break
            land += len(self.plate[p])
            self.continental += self.plate[p]
        return land

    #=========================================================================
    def lay_biomes(self):
        biome_size = self.config.biome_size
        continental = set(self.continental)
        biomes = []
        while continental:
            tile = random.choice(list(continental))
            biomes.append([tile])
            continental.remove(tile)
            while random.uniform(0, 1) > biome_size:
                cands = list(set(king_move(tile)) & continental)
                if cands:
                    tile = random.choice(cands)
                    biomes[-1].append(tile)
                    continental.remove(tile)
                else: break
        self.biomes = biomes

    #=========================================================================
    def lay_rivers(self):
        total_rivers = self.config.rivers
        continental = set(self.continental)
        coastal = set()
        for tile in continental:
            if not continental.issuperset(set(two_away(tile))):
                coastal.add(tile)

        def build_river():
            river = []
            banks = set()
            try: tile = random.choice(list(continental - coastal))
            except IndexError: tile = random.choice(list(continental))
            cands = set()
            forbidden = set()
            while True:
                river.append(tile)
                adjs = adjacent(tile)
                banks |= set(two_away(tile))
                c = set()
                for t in adjs:
                    if not t in continental:
                        return river, banks
                    if t in cands:
                        forbidden.add(t)
                    else:
                        cands.add(t)
                    if not continental.issuperset(set(adjacent(t))):
                        c.add(t)
                choices = c if len(c) > 0 else set(adjs)
                tile = random.choice(list(choices - forbidden))
        rivers = []
        while len(rivers) < total_rivers and len(continental) > 0:
            try: river, bank = build_river()
            except IndexError: continue
            rivers += reversed(river) #Reversing enables exact count
            continental -= set(river)
            coastal |= bank
        self.river = rivers[:total_rivers] #Exact count

    #=========================================================================
    def lay_terrain(self):
        biome_dist = list(self.config.biome_dist)
        biome_var = self.config.biome_var
        grid = [['o']*len(LONGS) for x in LATS]
        def lay_tile(tile, char):
            lat, long = tile
            if char not in 'orgpfdatsjmh':
                if type(biome_var[char]) == type([]):
                    from_pole = min(lat, len(LATS)-lat-1)
                    choice = list(biome_var[char][from_pole])
                elif type(biome_var[char]) == type(''):
                    choice = list(biome_var[char])
                char = random.choice(choice)
            grid[lat][long] = char
                    
        poles = [(lat,long) for lat in [0,1]+LATS[-2:] for long in LONGS]
        for tile in poles:
            lay_tile(tile, '-')
        for biome in self.biomes:
            char = random.choice(biome_dist)
            for tile in biome:
                lay_tile(tile, char)
        for tile in self.river:
            lay_tile(tile, 'r')
        self.map = grid

    #=========================================================================
    def build_world(self):
        """After initializing, this is the only method you need.
        Returns text map without trailing newline."""
        self.lay_plates()
        self.continental_plates()
        self.lay_biomes()
        self.lay_rivers()
        self.lay_terrain()
        return '\n'.join([''.join(c) for c in self.map])
        

#============================================================================
# Use termcolor module to return a colorized character cell for a tile.
on_color = { 
    'o': 'blue', 'r': 'blue', 'g': 'green', 'p': 'yellow',
    'f': 'green', 'd': 'yellow', 'a' : 'white', 't': 'white',
    's': 'grey', 'j': 'green', 'm': 'white', 'h': 'green'
}

def colorize(tile_char):
    attrs = []
    fg_color = 'grey'
    bg_color = on_color[tile_char]

    if tile_char in 'rgpath': 
        (fg_color, bg_color) = (bg_color, fg_color)
        attrs = ['bold', 'reverse']

    return colored(tile_char, fg_color, 'on_' + bg_color, attrs)

#=============================================================================
if __name__ == '__main__':
    config = Config()
    w = World(config)
    w.build_world()
    
    print '\n'.join(''.join([colorize(c) for c in row]) for row in w.map)

